/*
 * Copyright 2007 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.rebind.ui;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * Generates an implementation of a user-defined interface <code>T</code> that
 * extends {@link com.google.gwt.user.client.ui.Gadget}.
 * 
 * One method must be void setModulePrefs() and metadata starting with <code>@gadget.< prefname ></code>
 * where pref name is the name of a standard gadget module prefernce, followed by your
 * value for that preference. All other methods must be in the form getUserPref where
 * UserPref is the name of the user preference you would like to define. Metadata in teh same
 * format as in the previous example using standard user preference names. Do not use 
 * quotation marks around your preference values.
 */
public class GadgetGenerator extends Generator {

  private static final String GWT_QNAME = "com.google.gwt.core.client.GWT";

  private static final String GADGET_QNAME = "com.google.gwt.user.client.ui.Gadget";


  public GadgetGenerator() {
  }

  public String generate(TreeLogger logger, GeneratorContext context,
      String typeName) throws UnableToCompleteException {

    TypeOracle typeOracle = context.getTypeOracle();

    // Get metadata describing the user's class.
    JClassType userType = getValidUserType(logger, typeName, typeOracle);
    
    // Get the methods that correspond to UserPrefs.
    JMethod[] userPrefMethods = getValidUserPrefMethods(logger, userType);

    // Write the new class.
    String resultName = generateImpl(logger, context, userType, userPrefMethods);

    // Return the complete name of the generated class.
    return resultName;
  }

  private String computeSubclassName(JClassType userType) {
    String baseName = userType.getName().replace('.', '_');
    return baseName + "_generatedGadget";
  }

  private String generateImpl(TreeLogger logger, GeneratorContext context,
      JClassType userType, JMethod[] userPrefMethods)
      throws UnableToCompleteException {
    // Compute the package and class names of the generated class.
    String pkgName = userType.getPackage().getName();
    String subName = computeSubclassName(userType);

    // Begin writing the generated source.
    ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(
        pkgName, subName);
    f.addImport(GWT_QNAME);
    f.addImplementedInterface(userType.getQualifiedSourceName());

    PrintWriter pw = context.tryCreate(logger, pkgName, subName);
    if (pw != null) {
      SourceWriter sw = f.createSourceWriter(context, pw);

      // Build the gadget.xml file.
      GadgetBuilder bulder = new GadgetBuilder(subName.substring(0, subName.indexOf('_')));

      for (int i = 0; i < userPrefMethods.length; i++) {
        JMethod method = userPrefMethods[i];
        bulder.assimilate(logger, method);
      }

      // Write the gadget.xml into the output directory.
      String bundledImageUrl = bulder.writeGadget(logger, context, userType);


      // Generate an implementation of each method.
      for (int i = 0; i < userPrefMethods.length; i++) {
        JMethod method = userPrefMethods[i];
        generateUserMethod(logger, sw, method);
      }

      // Finish.
      sw.commit(logger);
    }

    return f.getCreatedClassName();
  }

  private void generateUserMethod(TreeLogger logger,
      SourceWriter sw, JMethod method)
      throws UnableToCompleteException {

    if(method.getName().equals("setModulePrefs")) {
     sw.indent();
     String methodName = method.getReadableDeclaration(false, true, true, true, true);
     sw.println(methodName + " {");
     sw.println("}");
     sw.outdent();
     
     return;
    }
    
    {
      sw.indent();
      String methodName = method.getReadableDeclaration(false, false, true, true, true);
      sw.println( methodName + " /*-{");
      {
        sw.indent();
     
        sw.print("return prefs.get");
        if(method.getReturnType().toString() == "boolean")
          sw.print("Bool");
        else if(method.getReturnType().toString() == "int")
          sw.print("Int");
        else
          sw.print(method.getReturnType().toString());
        sw.println("( " + method.getName().substring(3) + " );");
        sw.outdent();
      }
      sw.print("}-*/;");
      sw.outdent();
    }
  }

 
  private JMethod[] getValidUserPrefMethods(TreeLogger logger, JClassType userType)
      throws UnableToCompleteException {

    logger = logger.branch(TreeLogger.TRACE, "Analyzing methods on "
        + userType.getQualifiedSourceName(), null);


    Map rejectedMethodsAndWhy = new HashMap();
    JMethod[] leafMethods = userType.getOverridableMethods();
    for (int i = 0; i < leafMethods.length; i++) {
      JMethod method = leafMethods[i];

      if ( !method.getReturnType().getSimpleSourceName().equals("String") 
        && !method.getReturnType().getSimpleSourceName().equals("boolean")
        && !method.getReturnType().getSimpleSourceName().equals("int") 
        && !method.getReturnType().getSimpleSourceName().equals("void") ) {
        rejectedMethodsAndWhy.put(method, "Method cannot be of type " + method.getReturnType().getSimpleSourceName());
      }
    
      if (method.getParameters().length > 0) {
        rejectedMethodsAndWhy.put(method, "Method cannot take parameters");
        continue;
      }

    }

    // Make sure there aren't any invalid methods.
    if (!rejectedMethodsAndWhy.isEmpty()) {
      logger = logger.branch(TreeLogger.ERROR,
          "The following methods are invalid on a gadget:", null);
      for (Iterator iter = rejectedMethodsAndWhy.entrySet().iterator(); iter.hasNext();) {
        Map.Entry entry = (Map.Entry) iter.next();
        JMethod badMethod = (JMethod) entry.getKey();
        String reason = (String) entry.getValue();
        TreeLogger branch = logger.branch(TreeLogger.ERROR,
            badMethod.getReadableDeclaration(), null);
        branch.log(TreeLogger.ERROR, reason, null);
      }
      throw new UnableToCompleteException();
    }

    return leafMethods;
  }

  private JClassType getValidUserType(TreeLogger logger, String typeName,
      TypeOracle typeOracle) throws UnableToCompleteException {
    try {
      // Get the type that the user is introducing.
      JClassType userType = typeOracle.getType(typeName);

      // Get the type this generator is designed to support.
      JClassType magicType = typeOracle.findType(GADGET_QNAME);

      // Ensure it's an interface.
      if (userType.isInterface() == null) {
        logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName()
            + " must be an interface", null);
        throw new UnableToCompleteException();
      }

      // Ensure proper derivation.
      if (!userType.isAssignableTo(magicType)) {
        logger.log(TreeLogger.ERROR, userType.getQualifiedSourceName()
            + " must be assignable to " + magicType.getQualifiedSourceName(),
            null);
        throw new UnableToCompleteException();
      }

      return userType;

    } catch (NotFoundException e) {
      logger.log(TreeLogger.ERROR, "Unable to find required type(s)", e);
      throw new UnableToCompleteException();
    }
  }

}
